iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 3
1

在任何一種程式語言都有資料型別介紹,而此篇我們將來了解 Kotlin 在資料型別上的特性、操作、轉換等內容。

在 Kotlin 官方文件中有提到:

In Kotlin, everything is an object in the sense that we can call member functions and properties on any variable.

上述內容得知,Kotlin 的任何東西都是一個物件,可以存取任何對象的相關方法與屬性,不像 Java 有區分原始型別(Primitive Type)參考型態(Reference Types),在開發上有時候甚至需要做轉換才可使用。而 Kotlin 在宣告變數時使用的是靜態類型系統(static type system),即編輯器會按照變數類型辨識程式碼,判斷是否有存在類型與數值不符合的狀況發生,若有出現,編輯器會立即指出,例如下圖提示訊息:

https://ithelp.ithome.com.tw/upload/images/20200912/20121179s0fDvpLeBE.png

變數宣告

Kotlin 在變數宣告時主要會使用到兩種關鍵字 valvar

  • val 用於唯讀變數,一旦給值就無法再修改
  • var 用於需要重新修改數值的情況
fun main() {
	val readOnlyVariable = "鐵人賽第十二屆" // 宣告一個唯讀變數
	var playerName = "選手一號" // 宣告一個可重新修改數值的變數
	playerName = "選手二號" // 重新賦予新數值
}

Kotlin 官方這邊也有建議開發者在開發上建議優先使用 val,當遇到需要修改數值時再轉為 var 即可,若使用 var 宣告變數,開發者若沒有在程式中修改過,Intellij 編輯器也會提示建議改為 val,如下圖:
https://ithelp.ithome.com.tw/upload/images/20200912/20121179ihruzGQCee.png

空值型態

還記得嗎?我們在上一章有提到 Kotlin 有一個優勢是可以避免以前 Java 開發中常見的 NullPointerException 情況發生,主要原因是因為 Kotlin 預設宣告都只能是非 null 型態,例如以下範例,當我們想要進行指派 null 值給 String 時會發生編譯錯誤狀況:

https://ithelp.ithome.com.tw/upload/images/20200912/20121179hI5ojOeMew.png

這樣的錯誤檢查就能夠避免開發者經常會有出現錯誤的問題,而如果在開發情境上確實有必要使用 null 值,則可以將變數定義為 nullable 狀態,即在變數的型態定義上加上 ? 即可,如下範例:

fun main() {
    var test: String? = "鐵人賽"
    test = null
    println(test) // 印出 null
}

型別判斷處理

在介紹基本型別前,先介紹 Kotlin 在變數上有個特色是型別判斷處理,可對於已指派預設值的宣告變數自動定義型別,允許開發者省略型別定義,以下我們嘗試宣告一個變數,並輸出該變數的型別來看 Kotlin 是否有自動幫我們進行型別宣告,如下範例:

此範例先宣告變數 name 為「鐵人賽」,再利用「::class.simpleName」印出變數型別結果為 String

fun main() {
    val name = "鐵人賽"
    println(name::class.simpleName) // 印出 String -> 代表 Kotlin 自動幫我們定義型態
}

資料型別

Kotlin 在資料型別與 Java 非常相似,只差在變數型態必須使用首字大寫,型別分別如下:

  • 數值型別 Numbers (種類可依長度區分)

    • Byte (8 Bits)
    • Short (16 Bits)
    • Int (32 Bits)
    • Long (64 Bits)
    • Float (32 Bits)
    • Double (64 Bits)

    數值變數在操作上可直接宣告型態或是透過型別判斷進行操作:

    fun main() {
        val byte: Byte = 1
        val short: Short = 2
        val int: Int = 3
        val long: Long = 4L
        val float: Float = 5f
        val double: Double = 6.0
    
        println("Byte => $byte")
        println("Short => $short")
        println("Int => $int")
        println("Long => $long")
        println("Float => $float")
        println("Double => $double")
    }
    

    前面有提到 Kotlin 的一切都是物件,在以前 Java 變數型態有分為基本型別(Primitive type)參考型別(Reference type),即 intInteger 的差別,而在 J2SE 5.0 時有提供自動裝箱(autoboxing)拆箱(unboxing)來進行包裹基本型態,但在 Kotlin 中,只存在數值的裝箱,不存在拆箱,因為 Kotlin 是沒有存在基本資料型態的,下面將示範如何進行裝箱操作:

    此範例操作須搭配上面提到的概念-空值型態達成裝箱效果,會發現裝箱前與裝箱後的數值都一樣

    fun main() {
        val number: Int = 913
        val numberInBox: Int? = number
        println("裝箱前數值: $number , 裝箱後數值: $numberInBox")
        // 裝箱前數值: 913 , 裝箱後數值: 913
    }
    

    上面範例我們會發現兩個數值印出來雖然是相等的,但其實在 Kotlin 判斷數值是否相等有兩種比較方式(=====),== 是判斷數值是否相等, === 則是判斷兩個數值在記憶體位置是否相等,而其實 Kotlin 在變數裝箱操作時,記憶體位置會根據其資料型別的數值範圍進行定義,我們可以利用下面範例進行示範:

    我們會發現當 a 變數為 127 時,判斷兩個裝箱變數會為 true,因為 Int 型態定義數值範圍為 -128 ~ 127,當 b 變數超過 127 數值時,Kotlin 在記憶體分配上會有不同位置狀況發生。

    fun main() {
        val a: Int = 127
        val boxedA: Int? = a
        val anotherBoxedA: Int? = a
    
        val b: Int = 128
        val boxedB: Int? = b
        val anotherBoxedB: Int? = b
    
        println(boxedA === anotherBoxedA) // true
        println(boxedB === anotherBoxedB) // false
    }
    

    Kotlin 在數值轉換上有分顯性轉換與隱性轉換,隱性轉換即 Kotlin 會自動幫我們進行轉換,但若兩個數值為不同型態時,會自動以定義數值範圍較大的型態為轉換後的最終型態,例如以下範例:

    此範例為兩數相加,999為 Long 型態,1為 Int 型態,兩數相加後的結果 number 為 Long 型態

    fun main() {
        val number = 999L + 1
        println(number::class.simpleName) // 印出資料型別為 Long
    }
    

    而為了避免隱性轉換時自動選擇型態問題,我們在開發上可使用顯性轉換方式,即下面範例:

    fun main() {
        val number: Int = 65
        println(number.toByte())    // 印出 65
        println(number.toShort())   // 印出 65
        println(number.toLong())    // 印出 65
        println(number.toFloat())   // 印出 65.0
        println(number.toDouble())  // 印出 65.0
        println(number.toChar())    // 印出 A
        println(number.toString())  // 印出 65
    }
    
  • 字元型別 Char

    Char 表示字元類型,字元變數必須使用單引號(‘’)表示,在轉換上可利用顯性轉換為數字型態,如以下範例:

    fun main() {
        val char: Char = 'A'
        println(char.toInt())    // 印出 65
    }
    
  • 字串型別 String

    String 表示字串類型,在輸出時可使用字串模板表示式處理字串組成,再進行輸出,如下範例:

    fun main() {
        val username: String = "Devin"
        println("第十二屆鐵人賽 參加者 $username") // 印出「第十二屆鐵人賽 參加者 Devin」
    }
    
  • 布林型別 Boolean

    Boolean 表示為布林類型,其值有 truefalse

    fun main() {
        val isFalse: Boolean = false
        val isTrue: Boolean = true
    
        println(isFalse && isTrue) // 印出「false」
    }
    
  • 陣列型別 Array

    Kotlin 的 Array 型別在宣告上是以 Array<T> 表示,我們可以到 Kotlin 的 Array 型態定義查看,會發現原始型態已經幫我們定義 get、set、size 與 iterator 方法:

    https://ithelp.ithome.com.tw/upload/images/20200912/20121179oKLvyrROGy.png

    故我們在 Array 操作上可以如下範例進行操作:

    fun main() {
        val data: Array<Int> = arrayOf(1,2,3,4,5) // 宣告Array並賦予 1-5 數值
        data.forEach { println(it) }  // 利用 forEach 分別印出數值
    }
    

Const 作用

在前述有提到唯讀變數 val 不允許重新設定數值,但其實 val 是在程式執行階段(Run time)才進行賦值(Assign Value)動作,而我們若要限制程式在編輯階段(Compile time)就進行賦值動作,應使用 const 關鍵字搭配 val 進行變數宣告,我們可用一個範例來說明 constval 的差異:

https://ithelp.ithome.com.tw/upload/images/20200912/20121179O9kWjS1nt1.png

透過上面範例我們會發現兩件事:

  1. normalVariable 可利用 getRandomValue() 隨機取得 1 - 6 數值,表示程式是先在執行階段利用 getRandomValue() 方法取得數值後,才對 normalVariable 進行賦值
  2. 當我們嘗試將 constVariableFromGetValue 賦予 getRandomValue 方法時,會出現 const val 只能接受常數(constant value)

型別檢測與轉換

  • is 運算子

    is運算子可檢查物件或變數是否屬於某資料型別,如Int、String等,類似於Java的 instanceof

    fun main() {
        val data = "abc"
        println(data is String);   // 印出 true
        println(data is Any);      // 印出 true
    }
    
  • as 運算子進行型別轉換

    as運算子用於型別轉換,若要轉換的數值與指定型別相容,轉換就會成功;如果型別不相容,使用 as? 運算子就會返回值null,如下範例:

    fun main() {
        val x: Int = 2
        val y: Int = x as Int
        val z: String? = y as? String
    
        println(y) // 印出 2
        println(z) // 印出 null 
    }
    

特殊型別

除了上述基本型別以外,Kotlin 還有一些特殊型別運用於物件或函數上,這邊會先進行簡單介紹,會在後續章節介紹時會再深入說明:

1. Any 型別

根據 Kotlin 官方文件所述:

The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.

在此篇文章一開始介紹說明,Kotlin的一切都是物件,而每個物件其實都是繼承 Any 這個型別,此型別相當於Java的 Object 型別,而此型別也可再細分為 Any 與 Any?,Any屬於非空型別的根物件,Any?屬於可空型別的根物件。

2. Unit 型別

在 Java 中,當我們所設計的 function 不需回傳值時,我們會使用到 void 型別,而在 Kotlin 可使用 Unit 型別代替,而且若我們不特地為 function 設定回傳型態時,Kotlin 會自動幫我們預設型態為 Unit 型別,會返回 Unit 型別,例如以下範例。

fun main() {
    val username = getUserName()
    println(username::class.simpleName) // 印出 Unit 型別
}

fun getUserName() {

}

3. Nothing 型別

Nothing 型別其實類似於 Unit,Nothing 型別也是不返回任何東西,但差別在於 Nothing 型別意味著此函數不可能成功執行完成,只會拋出異常或是再也回不去函數呼叫的地方。

而 Nothing? 型別則會有一個使用情境,在 Java 中,void不能是變數的型別。也不能被當數值列印輸出。但是,在Java中有個包裝類Void是 void 的自動裝箱型別,如果我們想讓 function 返回型別永遠是 null 的話,可以把返回型別置為這個大寫的V的Void型別,而 Void 即對應 Kotlin 中的 Nothing? 型別。

範例(1) 使用 Nothing 型別

fun main() {
    getUserName() // 使用 Nothing 型別
}

fun getUserName(): Nothing {
    throw NotImplementedError() // 丟出異常
}

範例(2) 使用 Nothing? 型別

fun main() {
    getUserName() // 使用 Nothing? 型別
}

fun getUserName(): Nothing? {
    return null // 保持回傳 null
}

Kotlin 轉換 Java Code

有時候我們可能會好奇在 Kotlin 所撰寫的程式,實際轉換為 Java 會是怎麼樣的語法,此時我們可以利用 intellij 內建的工具進行轉換觀察。

在 Intellij 連續按 Shift 鍵兩次,搜尋「show kotlin」關鍵字,選擇「Show Kotlin Bytecode」,會出現Kotlin位元組碼工具視窗,再點擊「Decompile」按鈕即可觀看轉譯的Java 程式碼。

https://ithelp.ithome.com.tw/upload/images/20200912/20121179AFv0fulGrw.png

Reference


上一篇
[Day 02] 遠征預備 Kotlin × 開發環境介紹
下一篇
[Day 04] 遠征 Kotlin × 流程控制
系列文
30天從零撰寫 Kotlin 語言並應用於 Spring Boot 開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言